package search_backend

import (
	"fmt"
	"container/list"
	"gob"
	"os"
	"strings"
	"log"
	"math"
	"io/ioutil"
	"crypto/md5"
)

// Estructura que representa a un documento
type Doc struct {
	Title string
	Url string
	Sem float64
	Id string
}

const INDEX = "./indx/index"

// Almacenamiento de terminos por documento
var wordsMap map[string] map[string] int

// Diccionario documento - md5
var docKeys map[string] string
var keyDocs map[string] string

var cachedVectors map[string] map [string] int
var cachedVectorModules map[string] float64

// Lista de documentos
var documents *list.List

func AddDocument(document string) {
	if documents == nil {		
		documents = list.New()
		documents.PushFront(document)
	} else {
		var e *list.Element
		for e = documents.Front(); e != nil && fmt.Sprintf("%s", e.Value) != document; e = e.Next() {}
		if e == nil {
			documents.PushBack(document)
		}
	}
	if (docKeys == nil) {
		docKeys = make(map[string] string)
	}
	if (keyDocs == nil) {
		keyDocs = make(map[string] string)
	}
	md5 := calculateMd5(document)
	docKeys[document] = md5
	keyDocs[md5] = document
}

// Quita un documento de la lista de documentos (no elimina sus apariciones en el índice)
func RemoveDocument(doc string){
	if documents != nil {
		var e *list.Element
		for e = documents.Front(); e != nil && e.Value.(string) != doc; e = e.Next() {}
		if e != nil {
			if e.Value.(string) == doc {
				documents.Remove(e)
			}
		}
		docKeys[doc] = ""
		keyDocs[doc] = ""
	}
}

// Elimina una aparicion de un termino en un documento
func RemoveWord(document string, word string){
	if wordsMap != nil {
		if wordsMap[word][document] > 0 {
			wordsMap[word][document] = wordsMap[word][document] - 1
		}
	}
}

// Inserta un termino y se lo asocia al documento indicado
func AddWord(document string, word string){

	// Si el mapa no esta inicializado lo inicializamos
	if wordsMap == nil {
		wordsMap = make(map[string] map[string] int)
	} 
	// Si el termino no esta en el mapa lo insertamos
	if wordsMap[word] == nil {
		wordsMap[word] = make(map[string] int)
		wordsMap[word][document] = 0		
	}

	// Incrementamos el numero de apariciones del termino en el documento
	wordsMap[word][document] = wordsMap[word][document] + 1
}

// Imprime el contenido del indice de terminos 
func PrintWordsMap(){
	fmt.Println("\nTabla TERMINOS/APARICIONES\n------------------------")
	for key, wordAppearances := range wordsMap {
		fmt.Printf("%s\n", strings.ToUpper(key))
		for doc, count := range wordAppearances {
			fmt.Printf("Ͱ %s (%d apariciones)\n", doc, count)
		}
		fmt.Printf("𝈪\n\n")
	}
}

// Serializa el indice de documentos a un archivo
func SaveWordsMap () {
	// Guardamos el indice de terminos (wordsMap)
	f, err := os.Create(INDEX)
	if err != nil {
		println(err.String())
	}else{
		defer f.Close()
		encoder := gob.NewEncoder(f)
		encoder.Encode(wordsMap)
	}
	// Guardamos la lista de documentos
	f2, err2 := os.Create(INDEX + ".d")
	if err2 != nil {
		println(err2.String())
	}else{
		i := 0
		aux := make ([]string, documents.Len())
		for e := documents.Front(); e != nil; e = e.Next() {
			name := fmt.Sprintf("%s", e.Value)
			aux[i] = name
			i++
		}

		defer f2.Close()
		encoder := gob.NewEncoder(f2)
		encoder.Encode(aux)
	}
}

func WordsMapSize() int {
	return len(wordsMap)
}

// Carga un indice de documentos serializado de un archivo
func LoadWordsMap () { 
	f, err := os.Open(INDEX)
	if err != nil {
		println(err.String())
	} else {
		defer f.Close()

		var auxMap map[string] map[string] int

		decoder := gob.NewDecoder(f)
		decoder.Decode(&auxMap)

		var key string;
		var wordAppearances map[string] int;
		for key, wordAppearances = range auxMap {
			var doc string
			var count int
			for doc, count = range wordAppearances {
				for i := 0; i < count; i++ {
					AddWord(doc, key)
				}
			}			
		}		
	}
	// Cargamos la lista de documentos
	f2, err2 := os.Open(INDEX + ".d")
	if err2 != nil {
		println(err2.String())
	} else {
		defer f2.Close()
		decoder := gob.NewDecoder(f2)
		var aux []string
		decoder.Decode(&aux)
		for i := 0; i < len(aux) && len(aux[i]) > 0; i++ {
			AddDocument(aux[i])
		}
	}
}

// Realiza una búsqueda
func Search (query []string, init bool) map[int]Doc {
	if (!init) {
		log.Print("Executing query...")
	}

	// Tratamos la consulta como un documento
	AddDocument ("query")
	for i:= range query {
		AddWord ("query", query[i])
	}

	result := make (map[int] Doc)
	i := 0

	// Comparamos la consulta con todos los documentos
	var e *list.Element
	for e = documents.Front(); e != nil; e = e.Next() {
		if fmt.Sprintf("%s", e.Value) != "query" {
			sem := sem("query", fmt.Sprintf("%s", e.Value))
			if sem > 0 {
				url := fmt.Sprintf("%s", e.Value)
				title := url[strings.LastIndex(url, "/")+1:]
				result[i] = Doc{Title: title, Url: url, Sem: sem, Id: docKeys[url]} 
				i++
			}
		}
	}

	// Quitamos el documento auxiliar del indice
	RemoveDocument("query")
	for i:= range query {
		RemoveWord("query", query[i])
	}
	cachedVectors["query"] = nil
	cachedVectorModules["query"] = -1
	orderedResults := orderResults(result)

	return orderedResults
}

// Genera un archivo XML con los resultados de una búsqueda
func WriteResultsToXML(path string, content string) {
	err := ioutil.WriteFile(path, []uint8(content), 0666)
	if err != nil {
		log.Println("Error while writing results XML file")
	}
}

// Ordena el resultado de una busqueda
func orderResults (result map[int] Doc) map[int] Doc {

	sortedDocs := list.New()
	for _, result := range result {
		var e *list.Element
		for e = sortedDocs.Front(); e != nil && e.Value.(Doc).Sem > result.Sem; e = e.Next() {}
		if e == nil {
			sortedDocs.PushBack(result)
		} else { 
			if fmt.Sprintf("%s", e.Value.(Doc).Title) != result.Title {
				sortedDocs.InsertBefore(result, e)
			}
		}
	}
	
	toReturn := make(map[int] Doc)
	i := 0
	for e := sortedDocs.Front(); e != nil; e = e.Next(){
		toReturn[i] = e.Value.(Doc)
		i++
	}

	return toReturn
}

// Genera el vector que representa a un documento (con el numero de apariciones de cada termino)
func genVector (docTitle string) map[string] int {	
	vector := make(map[string]int)
	for key, wordAppearances := range wordsMap {	
		for doc, num := range wordAppearances {		
			if(docTitle == doc && num > 0) {
				vector[key] = num
			}
		}			
	}
	return vector
}

func genModule (docTitle string) float64 {
		module := float64(0)
		vector := cachedVectors[docTitle]
		for key := range vector {
			module += float64(vector[key] * vector[key])
		}
		module = math.Sqrt(module)
		return module
}

// Calcula la semejanza entre dos vectores de términos
func sem (doc1 string, doc2 string) float64 {

	numerador_channel := make (chan float64)
	den1_channel := make (chan float64)

	if (cachedVectors == nil) {
		cachedVectors = make (map[string] map [string] int)
		cachedVectorModules = make (map[string] float64)
	}

	vector1 := cachedVectors[doc1]
	vector1module := cachedVectorModules[doc1]
	if (vector1 == nil) {
		vector1 = genVector (doc1)
		cachedVectors[doc1] = vector1
		vector1module = -1
		go func () {
			den1 := float64(0)
			for key := range vector1 {
				den1 += float64(vector1[key] * vector1[key])
			}
			den1 = math.Sqrt(den1)
			cachedVectorModules[doc1] = den1
			den1_channel <- den1
		}()
	}

	vector2 := cachedVectors[doc2]
	vector2module := cachedVectorModules[doc2]
	if (vector2 == nil) {
		vector2 = genVector (doc2)
		cachedVectors[doc2] = vector2
		vector2module = genModule (doc2)
		cachedVectorModules[doc2] = vector2module
	}

	go func () {
		numerador := float64(0)
		if (len(vector1) > len(vector2)) {
			for key := range vector2 {
				if (vector1[key] == 0) {
					continue
				}
				numerador += float64(vector1[key] * vector2[key])
			}
			numerador_channel <- numerador
		} else {
			for key := range vector1 {
				if (vector2[key] == 0) {
					continue
				}
				numerador += float64(vector1[key] * vector2[key])
			}
			numerador_channel <- numerador
		}
	}()

	
	numerador := <- numerador_channel
	if (vector1module == -1) {
		vector1module = <- den1_channel
	}

	sem := float64(0)
	if (vector1module != 0 && vector2module != 0) {
		sem = numerador / float64 (vector1module * vector2module)
	}

	return sem
}

func calculateMd5 (docPath string) string {
	contents, _ := ioutil.ReadFile (docPath)
	hash := md5.New()
	hash.Write([]byte(contents))
	md5sum := hash.Sum ()
	result := ""
	for _, number := range md5sum {
		chunk := fmt.Sprintf("%d", number)
		result += chunk
	}
	return result
}

func GetDocKey (docPath string) string {
	return docKeys[docPath]
}

func GetDocFromKey (key string) string {
	return keyDocs[key]
}

func InvalidateCaches () {
	cachedVectors = nil
	cachedVectorModules = nil
}
